package canglang.scan.core.base.taglib.displaytag.tags;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.LongRange;
import org.apache.commons.lang.math.Range;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.views.jsp.TagUtils;
import canglang.scan.core.base.taglib.displaytag.model.Cell;
import canglang.scan.core.base.taglib.displaytag.model.Column;
import canglang.scan.core.base.taglib.displaytag.model.HeaderCell;
import canglang.scan.core.base.taglib.displaytag.model.Row;
import canglang.scan.core.base.taglib.displaytag.model.TableModel;
import canglang.scan.core.base.taglib.displaytag.pagination.PaginatedList;
import canglang.scan.core.base.taglib.displaytag.pagination.PaginatedListSmartListHelper;
import canglang.scan.core.base.taglib.displaytag.pagination.SmartListHelper;
import canglang.scan.core.base.taglib.displaytag.properties.TableProperties;
import canglang.scan.core.base.taglib.displaytag.render.HtmlTableWriter;
import canglang.scan.core.base.taglib.displaytag.util.Href;
import canglang.scan.core.base.taglib.displaytag.util.RequestHelper;
import canglang.scan.core.base.taglib.displaytag.util.RequestHelperFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;


/**
 * This tag takes a list of objects and creates a table to display those objects. With the help of column tags, you
 * simply provide the name of properties (get Methods) that are called against the objects in your list that gets
 * displayed. This tag works very much like the struts iterator tag, most of the attributes have the same name and
 * functionality as the struts tag.
 *
 * @author mraible
 * @author Fabrizio Giustina
 * @version $Revision: 1160 $ ($Author: fgiust $)
 */
public class TableTag extends HtmlTableTag {

    /**
     * D1597A17A6.
     */
    private static final long serialVersionUID = 899149338534L;

    /**
     * logger.
     */
    private static Log log = LogFactory.getLog(TableTag.class);

    /**
     * RequestHelperFactory instance used for link generation.
     */
    private static RequestHelperFactory rhf;

    /**
     * Object (collection, list) on which the table is based. This is not set directly using a tag attribute and can be
     * cleaned.
     */
    protected Object list;

    // -- start tag attributes --

    /**
     * Object (collection, list) on which the table is based. Set directly using the "list" attribute or evaluated from
     * expression.
     */
    protected Object listAttribute;

    /**
     * actual row number, updated during iteration.
     */
    private int rowNumber = 1;

    /**
     * name of the object to use for iteration. Can contain expressions.
     */
    private String name;

    /**
     * Unique table id.
     */
    private String uid;

    /**
     * Use form post in paging/sorting links (javascript required).
     */
    private String form;

    private String hideTotalRec;

    private String showAnchor;

    private String action;

    // -- end tag attributes --

    /**
     * table model - initialized in doStartTag().
     */
    private TableModel tableModel;

    /**
     * current row.
     */
    private Row currentRow;

    /**
     * Used by various functions when the person wants to do paging - cleaned in doEndTag().
     */
    private SmartListHelper listHelper;

    /**
     * base href used for links - set in initParameters().
     */
    private Href baseHref;

    /**
     * table properties - set in doStartTag().
     */
    private TableProperties properties;

    /**
     * Iterator on collection.
     */
    private Iterator tableIterator;

    private String styleClass;

    public String getShowAnchor() {
        return showAnchor;
    }

    public void setShowAnchor(String showAnchor) {
        this.showAnchor = showAnchor;
    }

    public String getHideTotalRec() {
        return hideTotalRec;
    }

    public void setHideTotalRec(String hideTotalRec) {
        this.hideTotalRec = hideTotalRec;
    }

    public String getStyleClass() {
        return styleClass;
    }

    public void setStyleClass(String styleClass) {
        this.styleClass = styleClass;
    }

    /**
     * Is this the last iteration we will be performing? We only output the footer on the last iteration.
     */
    private boolean lastIteration;

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    /**
     * Included row range. If no rows can be skipped the range is from 0 to Long.MAX_VALUE. Range check should be always
     * done using containsLong(). This is an instance of org.apache.commons.lang.math.Range, but it's declared as Object
     * to avoid runtime errors while Jasper tries to compile the page and commons lang 2.0 is not available. Commons
     * lang version will be checked in the doStartTag() method in order to provide a more user friendly message.
     */
    private Object filteredRows;

    /**
     * The paginated list containing the external pagination and sort parameters The presence of this paginated list is
     * what determines if external pagination and sorting is used or not.
     */
    private PaginatedList paginatedList;

    /**
     * Is this the last iteration?
     *
     * @return boolean <code>true</code> if this is the last iteration
     */
    protected boolean isLastIteration() {
        return this.lastIteration;
    }

    /**
     * Is the current row empty?
     *
     * @return true if the current row is empty
     */
    protected boolean isEmpty() {
        return this.currentRow == null;
    }

    public void setForm(String form) {
        this.form = form;
    }


    /**
     * Used to directly set a list (or any object you can iterate on).
     *
     * @param value Object
     * @deprecated use setName() to get the object from the page or request scope instead of setting it directly here
     */
    public void setList(Object value) {
        this.listAttribute = value;
    }

    /**
     * Sets the name of the object to use for iteration.
     *
     * @param value name of the object to use for iteration (can contain expression). It also supports direct setting of
     *              a list, for jsp 2.0 containers where users can set up a data source here using EL expressions.
     */
    public void setName(Object value) {
        if (value instanceof String) {
            // ok, assuming this is the name of the object
            this.name = (String) value;
        } else {
            // is this the list?
            this.list = value;
        }
    }

    /**
     * Sets the name of the object to use for iteration. This setter is needed for jsp 1.1 container which doesn't
     * support the String - Object conversion. The bean info class will swith to this setter.
     *
     * @param value name of the object
     */
    public void setNameString(String value) {
        this.name = value;
    }

    /**
     * Sets the unique id used to identify for this table.
     *
     * @param value String
     */
    public void setUid(String value) {
        this.uid = value;
    }

    /**
     * Returns the unique id used to identify for this table.
     *
     * @return id for this table
     */
    public String getUid() {
        return this.uid;
    }

    /**
     * Returns the properties.
     *
     * @return TableProperties
     */
    protected TableProperties getProperties() {
        return this.properties;
    }

    /**
     * Returns the base href with parameters. This is the instance used for links, need to be cloned before being
     * modified.
     *
     * @return base Href with parameters
     */
    protected Href getBaseHref() {
        return this.baseHref;
    }


    /**
     * Called by interior column tags to help this tag figure out how it is supposed to display the information in the
     * List it is supposed to display.
     *
     * @param column an internal tag describing a column in this tableview
     */
    public void addColumn(HeaderCell column) {
        if (log.isDebugEnabled()) {
            log.debug("[" + getUid() + "] addColumn " + column);
        }

        if ((this.paginatedList != null) && (column.getSortable())) {
            String sortCriterion = paginatedList.getSortCriterion();

            String sortProperty = column.getSortProperty();
            if (sortProperty == null) {
                sortProperty = column.getBeanPropertyName();
            }

            if ((sortCriterion != null) && sortCriterion.equals(sortProperty)) {
                this.tableModel.setSortedColumnNumber(this.tableModel.getNumberOfColumns());
                column.setAlreadySorted();
            }
        }

        this.tableModel.addColumnHeader(column);
    }

    /**
     * Adds a cell to the current row. This method is usually called by a contained ColumnTag
     *
     * @param cell Cell to add to the current row
     */
    public void addCell(Cell cell) {
        // check if null: could be null if list is empty, we don't need to fill rows
        if (this.currentRow != null) {
            int columnNumber = this.currentRow.getCellList().size();
            this.currentRow.addCell(cell);

            // just be sure that the number of columns has not been altered by conditionally including column tags in
            // different rows. This is not supported, but better avoid IndexOutOfBounds...
            if (columnNumber < tableModel.getHeaderCellList().size()) {
                HeaderCell header = tableModel.getHeaderCellList().get(columnNumber);
                header.addCell(new Column(header, cell, currentRow));
            }
        }
    }

    /**
     * Is this the first iteration?
     *
     * @return boolean <code>true</code> if this is the first iteration
     */
    protected boolean isFirstIteration() {
        if (log.isDebugEnabled()) {
            log.debug("["
                    + getUid()
                    + "] first iteration="
                    + (this.rowNumber == 1)
                    + " (row number="
                    + this.rowNumber
                    + ")");
        }
        // in first iteration this.rowNumber is 1
        // (this.rowNumber is incremented in doAfterBody)
        return this.rowNumber == 1;
    }

    /**
     * When the tag starts, we just initialize some of our variables, and do a little bit of error checking to make sure
     * that the user is not trying to give us parameters that we don't expect.
     *
     * @return int
     * @throws JspException generic exception
     * @see javax.servlet.jsp.tagext.Tag#doStartTag()
     */
    public int doStartTag() throws JspException {


        this.properties = TableProperties.getInstance((HttpServletRequest) pageContext.getRequest());

        this.tableModel = new TableModel(this.properties, pageContext.getResponse().getCharacterEncoding(), pageContext);

        this.tableModel.setId(getUid());

        this.tableModel.setForm(this.form);

        this.tableModel.setAction(this.action);
        this.list = evaluateExpression(this.name);

        this.paginatedList = (PaginatedList) this.list;

        this.list = this.paginatedList.getList();

            HashMap errorMap = (HashMap) TagUtils.getStack(pageContext).findValue("fieldErrors");
            if (errorMap.size() > 0) {
                this.paginatedList.setPageNumber(this.paginatedList.getOldPageNumber());
                this.paginatedList.setSortCriterion(this.paginatedList.getOldSortCriterion());
                this.paginatedList.setSortDirection(this.paginatedList.getOldSortDirection());
            }else {
                this.paginatedList.setOldPageNumber(this.paginatedList.getPageNumber());
                this.paginatedList.setOldSortCriterion(this.paginatedList.getSortCriterion());
                this.paginatedList.setOldSortDirection(this.paginatedList.getSortDirection());
        }

        this.tableModel.setSortOrderAscending("asc".equalsIgnoreCase(paginatedList.getSortDirection()));

        this.tableIterator = IteratorUtils.getIterator(this.list);

        filteredRows = new LongRange(1, Long.MAX_VALUE);

        if (rhf == null) rhf = this.properties.getRequestHelperFactoryInstance();

        RequestHelper requestHelper = rhf.getRequestHelperInstance(this.pageContext);

        this.baseHref = requestHelper.getHref();

        doIteration();

        return 2;
    }

    /**
     * @see javax.servlet.jsp.tagext.BodyTag#doAfterBody()
     */
    public int doAfterBody() {

        if (log.isDebugEnabled()) {
            log.debug("[" + getUid() + "] doAfterBody called - iterating on row " + this.rowNumber);
        }

        // increment this.rowNumber
        this.rowNumber++;

        // Call doIteration() to do the common work
        return doIteration();
    }

    /**
     * Utility method that is used by both doStartTag() and doAfterBody() to perform an iteration.
     *
     * @return <code>int</code> either EVAL_BODY_TAG or SKIP_BODY depending on whether another iteration is desired.
     */
    protected int doIteration() {

        if (log.isDebugEnabled()) {
            log.debug("[" + getUid() + "] doIteration called");
        }

        // Row already filled?
        if (this.currentRow != null) {
            // if yes add to table model and remove
            this.tableModel.addRow(this.currentRow);
            this.currentRow = null;
        }

        if (this.tableIterator.hasNext()) {

            Object iteratedObject = this.tableIterator.next();
            if (getUid() != null) {
                if ((iteratedObject != null)) {
                    // set object into this.pageContext
                    if (log.isDebugEnabled()) {
                        log.debug("[" + getUid() + "] setting attribute \"" + getUid() + "\" in pageContext");
                    }
                    this.pageContext.setAttribute(getUid(), iteratedObject);

                } else {
                    // if row is null remove previous object
                    this.pageContext.removeAttribute(getUid());
                }
                // set the current row number into this.pageContext
                this.pageContext.setAttribute(getUid() + "_rowNum", this.rowNumber);
            }

            // Row object for Cell values
            this.currentRow = new Row(iteratedObject, this.rowNumber);

            this.lastIteration = !this.tableIterator.hasNext();

            // new iteration
            // using int to avoid deprecation error in compilation using j2ee 1.3
            return 2;
        }
        this.lastIteration = true;

        if (log.isDebugEnabled()) {
            log.debug("[" + getUid() + "] doIteration() - iterator ended after " + (this.rowNumber - 1) + " rows");
        }

        // end iteration
        return SKIP_BODY;
    }


    /**
     * Is the current row included in the "to-be-evaluated" range? Called by nested ColumnTags. If <code>false</code>
     * column body is skipped.
     *
     * @return <code>true</code> if the current row must be evaluated because is included in output or because is
     *         included in sorting.
     */
    protected boolean isIncludedRow() {
        return ((Range) filteredRows).containsLong(this.rowNumber);
    }


    /**
     * Draw the table. This is where everything happens, we figure out what values we are supposed to be showing, we
     * figure out how we are supposed to be showing them, then we draw them.
     *
     * @return int
     * @throws JspException generic exception
     * @see javax.servlet.jsp.tagext.Tag#doEndTag()
     */
    public int doEndTag() throws JspException {

        setupViewableData();

        int returnValue = EVAL_PAGE;

        writeHTMLData();

        cleanUp();

        return returnValue;
    }


    /**
     * clean up instance variables, but not the ones representing tag attributes.
     */
    private void cleanUp() {
        // reset instance variables (non attributes)
        this.baseHref = null;
        this.currentRow = null;
        this.listHelper = null;
        this.properties = null;
        this.rowNumber = 1;
        this.tableIterator = null;
        this.tableModel = null;
        this.list = null;
        this.paginatedList = null;
    }





    /**
     * This sets the list of all of the data that will be displayed on the page via the table tag. This might include
     * just a subset of the total data in the list due to to paging being active, or the user asking us to just show a
     * subset, etc...
     */
    @SuppressWarnings("unchecked")
    protected void setupViewableData() {

        List fullList = this.tableModel.getRowListFull();

        this.listHelper = new PaginatedListSmartListHelper(this.paginatedList, this.properties);

        this.tableModel.setRowListPage(fullList);
    }

    /**
     * Uses HtmlTableWriter to write table called when data have to be displayed in a html page.
     *
     * @throws JspException generic exception
     */
    protected void writeHTMLData() throws JspException {
        JspWriter out = this.pageContext.getOut();

        String css = this.properties.getCssTable();
        if (StringUtils.isNotBlank(styleClass)) {
            this.addClass(styleClass+",");
        }
        if (StringUtils.isNotBlank(css)) {
            this.addClass(css);
        }

        HttpServletRequest req=(HttpServletRequest)this.pageContext.getRequest();
        String cPath=req.getContextPath();
        // use HtmlTableWriter to write table
        new HtmlTableWriter(
                this.tableModel,
                this.properties,
                this.baseHref,
                out,
                this.paginatedList,
                this.listHelper,
                getAttributeMap(),
                this.uid, this.name,cPath,hideTotalRec,showAnchor).writeTable(this.tableModel, this.getUid());
    }


    /**
     * Get the table model for this tag. Sometimes required by local tags that cooperate with DT. USE THIS METHOD WITH
     * EXTREME CAUTION; IT PROVIDES ACCESS TO THE INTERNALS OF DISPLAYTAG, WHICH ARE NOT TO BE CONSIDERED STABLE PUBLIC
     * INTERFACES.
     *
     * @return the TableModel
     */
    public TableModel getTableModel() {
        return this.tableModel;
    }

    /**
     * Called by the setProperty tag to override some default behavior or text String.
     *
     * @param propertyName  String property name
     * @param propertyValue String property value
     */
    public void setProperty(String propertyName, String propertyValue) {
        this.properties.setProperty(propertyName, propertyValue);
    }

    /**
     * @see javax.servlet.jsp.tagext.Tag#release()
     */
    public void release() {
        if (log.isDebugEnabled()) {
            log.debug("[" + getUid() + "] release() called");
        }

        super.release();

        // tag attributes
        this.listAttribute = null;
        this.name = null;
        this.filteredRows = null;
        this.uid = null;
        this.form = null;
    }

    /**
     * Returns the name.
     *
     * @return String
     */
    protected String getName() {
        return this.name;
    }

    private TableModel doExportIteration(TableModel exportModel, Iterator exportIterator) {
        Row exportRow;
        int i = 1;
        while (exportIterator.hasNext()) {
            Object iteratedObject = exportIterator.next();
            exportRow = new Row(iteratedObject, i);
            exportModel.addRow(exportRow);
            i++;
        }
        return exportModel;
    }

}
